Online documentation - WebsydianExpress v3.5 |
Developers can perform a number of tasks to avoid or detect errors in the Plex production environment. Checking *Call status and *Returned status is not difficult but it can be time consuming, tedious and difficult to perform systematically. And what do you do when a function call returns *Instance not found?
In WebsydianExpress we have created a Catch feature which facilitates writing solid code for Plex applications. The purpose is to systematically detect, handle and report errors in business processes before they reach the production environment - and in case they should have gone undetected into production.
The method is in no way compulsory. It is meant as an inspiration for creating code where unexpected errors are easier to detect and investigate. We recommend that you use the parts you find relevant - and to further develop the method yourself according to your specific needs.
You may already have solved the issues that this document addresses, but there may still be a couple of pointers you can use.
In our experience, generally two factors are most likely to cause problems in the production environment:
Of course there are a lot of other reasons for errors, but these two seem to occur more often in production environments - and they can often be very hard to identify as the errors often only occurs in very special situations.
The abstract WebsydianExpress functions contain meta-code that initializes all local and global fields (see for instance Abstract.FunctionShell, Pre Point Start initialize). At this point the Environment<*Returning status> is also set to *Successful.
When you create functions that do not inherit from these abstract functions, we strongly recommend that you create an abstract function that clears these variables - and then let all your functions inherit from the abstract function you created.
However, you must consider carefully whether this is feasible before clearing the variables as there are a few functions that depend on the fact that the values of the fields are maintained between calls.
Most developers are very aware that they should check the Environment<*Returned Status> after a program call. Still, some times this is not done.
While the returned status in most cases is handled correctly, the situation is quite different when it comes to the call status.
An abnormal call status usually indicates a non-existing function object. In most of the models we have seen, the call status are not checked at all - or only very rarely.
The problem of not checking the call status is that the returned status of a call to a non-existing program actually is successful. So if you only check the returned status, the calling program will continue as after a successful call.
This means that to solve the error-checking problem fully, you must check both the returned status and the call status - and the program must take a default action if unhandled errors occur.
An easy way to help in solving the error checking issue, the abstract WebsydianExpress functions contain a subroutine called "Catch".
This subroutine checks both the call status and the returned status.
If the call status isn't *Normal or the returned status isn't successful, an error is written to the message log, a text message is generated for the end user, and the program terminates with the *Returning Status = SignalTerminate (ERROR).
The message that is written to the message log contains the identification of the calling function (the current function) - and the called function. The called function is taken from the field Environment<*Object> - which means that you have to populate this field before using the Catch subroutine.
The aim is to ensure that all unexpected errors are registered in the message log and that processing is terminated when an unexpected error occurs.
A very common case is that you have a function that should never return an error.
Use the Catch subroutine as follows:
Name: Function: MyFunction, Environment<*Object>, Scoped
Call MyFunction
Go Sub Catch
If at any time MyFunction is not available (e.g. when deploying in the production environment), the Catch subroutine will check the call status - and report an error in the message log. As you have populated the field Environment<*Object> with the name of MyFunction, the message will contain both name of the calling and the called function (the name of the calling function is resolved by meta-code).
The assumption is that MyFunction always return successful in the returned status. If at any time this does not happen, it is recorded in the message log.
The Catch subroutine checks both status fields and reports any errors to the message log. At the same time, the calling function terminates and returns a special error code. The function also generates a message for the end user and places it on the message stack
Whether this message will be shown depends on the handling of the calling programs in the call hierarchy. If the calling programs do not check for errors and at some point show an error page, the error will never be shown to the user.
In some cases, you are willing to accept any error from a function (e.g. if you are just retrieving some informational data that are not vital for the further processing).
Even though you are willing to accept any error, you should not accept a missing program - so you should still check the call status.
You can perform this check like this:
Name: Function: MyFunction, Environment<*Object>, Scoped
Call MyFunction
If Environment<*Call status> != <*Call status.*Normal>
Go Sub Catch
If the call status is abnormal, the Catch routine writes the error to the message log.
Many functions can return a non-successful return status without it being an error.
The most obvious example is a SingleFetch function.
A SingleFetch function will return *Instance not found if the key has no corresponding record.
In some cases, this can be a serious error - but in many cases, this is a perfectly acceptable situation.
The easy way to check for this is as follows:
Name: Function: MyFunction, Environment<*Object>, Scoped
Call MyFunction
Case
When Environment<*Call status> != <*Call status.*Normal>
Go Sub Catch
When Environment<*Returned status> == <*Returned status.*Successful>
When Environment<*Returned status> == <*Returned status.*Instance not found>
Otherwise
Go Sub Catch
In most cases, you would also specify processing for the successful and the instance not found return codes - but this is irrelevant in this case.
By entering this code, you ensure that any abnormal call status or any unexpected returned status is reported in the message log.
In most Plex models we have seen, the function just return one of the standard values of the *Returned Status field. The normal usage is that if no error has occurred, the status is set to *Successful; if an error has occurred, the status is set to *Error; if a function has tried to read a non-existing record, the status will be set to *Instance not found.
In many situations, this is all you need. However, all errors are not necessarily created equal - so in some situations, you might want to report a more specific error to the calling function - and maybe also make the administrator aware of the problem.
If the called function encounters an error it can't handle itself, it can report this to the message log by writing a message that describes the error to the message log.
We recommend that you create a special function inheriting from Abstract.CreateLogMessage for each error you want to be able to report to the log.
Writing the message to the message log ensures that the administrator is made aware that the error has happened - this can also be very helpful when you develop your applications.
The other thing you might want to do, is to inform the calling function of the exact type of error that has occurred.
The CreateLogMessage returns a message id as an output field. This identifies the created message record in the message log.
This field inherits from *Returned Status - so you can assign the message id to the Environment<*Returning Status> field.
After this, terminate the function.
The calling function can use this message id in a number of ways:
The EventHandler HandleOrdersForCustomer calls the service function FindOrdersForCustomer.
The FindOrdersForCustomer has a customer id as an input field and it starts by reading some information for this customer.
If customer does not exist, the FindOrdersForCustomer function must be able to reports this.
To be able to report this, the developer of the FindOrdersForCustomer function creates a CreateLogMessage function.
Source Object | Verb | Target Object |
---|---|---|
CustomerDoesNotExist | is a FNC | Abstract.CreateLogMessage |
CustomerDoesNotExist | implement SYS | Yes |
CustomerDoesNotExist | input field FLD | CustomerID |
CustomerDoesNotExist | option NME | WSYAPI/Error |
Enter the following text in the CustomerDoesNotExist.MessageText source code:
The specified customer &(1:) does not exist.
The option triple determines the severity of the message in the message log. If you do not specify an option, the severity will be "Informative".
Add the following code to throw the exception in the FindOrdersForCustomer function:
Name: Function: Customer.Fetch.SingleFetch, Environment<*Object>, Scoped
Call Customer.Fetch.SingleFetch
Case
When Environment<*Call status> != <*Call status.*Normal>
Go Sub Catch
When Environment<*Returned status> == <*Returned status.*Successful>
When Environment<*Returned status> == <*Returned status.*Instance not found>
Name: Function: CustomerDoesNotExist, Environment<*Object>, Scoped
Call CustomerDoesNotExist
Go Sub Catch
Environment<*Returning status> = CustomerDoesNotExist/Output<APIFields.MessageID>
Go Sub Terminate
Otherwise
Go Sub Catch
This would for example be relevant if HandleOrdersForCustomer had just read the customer that it had specified as input to FindOrdersForCustomer.
In this case, it would be a most unexpected situation that FindOrdersForCustomer did not find the customer - but not completely impossible.
In this case, you must inform the user that his processing has failed - and you might want to get the user to contact the system administrator to start the identification and handling of the error.
Create the following message:
Source Object | Verb | Target Object |
---|---|---|
HandleOrdersForCustomer | Message MSG | UnexpectedError |
HandleOrdersForCustomer.UnexpectedError | Parameter FLD | WSYAPI/APIFields.MessageID |
With the text:
An unexpected error occurred. Please contact the administrator. Error code &(1:).
Name: Function: FindOrdersForCustomer, Environment<*Object>, Scoped
Call FindOrdersForCustomer
If Environment<*Call status> != <*Call status.*Normal>
Go Sub Catch
If Environment<*Returned Status> != <*Returned Status *Successful>
Format Message Message: HandleOrdersForCustomer.UnexpectedError, Environment<*Message Text>
Map with Environment<*Returned Status>
Go Sub Send message
Go Sub ErrorHandler
This way, you inform the user that his action has failed - and if the user contacts the administrator, the administrator will be able to find the exact message in the message log.
The user has been informed that an error occurred butt has not been informed which kind of error (which will often be desirable for external users).
This could be relevant if the user has entered the user id - or maybe if the user is the person who can investigate why the customer does not exist.
Name: Function: FindOrdersForCustomer, Environment<*Object>, Scoped
Call FindOrdersForCustomer
If Environment<*Call status> != <*Call status.*Normal>
Go Sub Catch
If Environment<*Returned Status> != <*Returned Status *Successful>
APIServer.Messages.GetTextForMessage
Map with:
Environment<*Returned Status>
Language<APIFields.LanguageCode>
APIServer.Session.GetBasicSessionData/Output<APIFields.SiteKey>
Cast Environment<*Message Text>, APIServer.Messages.GetTextForMessage
Go Sub Send message
Go Sub ErrorHandler
In this way, you show the log message to the end user (possibly translated to the language of the user) - and he can react.
You can do this in two ways. You can either use the APIServer.Messages.GetMessageType function to retrieve the message type and code a case structure that can handle the different errors, or you can use the Catch subroutine to resolve the message type.
As the use of the GetMessageType API is quite simple, this describes the second approach.
If you just call the Catch subroutine after the FindOrdersForCustomer function, any message id will be reported and handled as an unexpected error (see the basic use of the Catch subroutine above.
However, you can override this behavior - and at the same time instruct the Catch subroutine to resolve the message type by adding the field "e" scoped by the implemented CreateLogMessage function.
Source Object | Verb | Target Object |
---|---|---|
HandleOrdersForCustomer | local FLD
...for VAR |
CustomerDoesNotExist.e
WSYAPI/Catch |
Name: Function: FindOrdersForCustomer, Environment<*Object>, Scoped
Call FindOrdersForCustomer
Go Sub Catch
If Environment<*ReturnedEventType> == Catch<CustomerDoesNotExist.e>
* Enter handling of the error here
The end-result of this is:
When the FindOrdersForCustomer finds that the customer does not exist, it will return the message id of a message of the type CustomerDoesNotExist in the status field.
In the HandleOrdersForCustomer, the Catch subroutine finds the message type of the returned message and compares it with message types in the Catch variable. As the message type is found, the Catch subroutine populates the field Environment<*ReturnedEventType> with the message type id of the message.
Now, the developer can enter his own error handling for this specific event - or he can choose to ignore it completely.
If FindOrdersForCustomer is returning other message types, HandleOrdersForCustomer will report this in the log, and the developer can start handling these errors.
One thing you need to notice is that when you add the "e" field of an event to the Catch variable, you take responsibility for handling this message type everywhere in the calling function.